day 5¶

このノートブックの実行例はこちら(HTML版)で確認できます


0. はじめに¶

ページ上部のメニューバーにある Kernel メニューをクリックし、プルダウンメニューから [Change Kernel ...] を選び、gssm2024:Python を選択してください。

No description has been provided for this image

ノートブック上部の右隅に表示されたカーネル名が gssm2024:Python になっていることを確認してください。

No description has been provided for this image

1. テキスト分析 (実践編)¶

1.0 事前準備¶

1.0.1 定義済み関数の読み込み¶

以下のセルを修正せずに実行してください

In [1]:
import warnings
warnings.simplefilter('ignore')

import gssm_utils

%matplotlib inline

1.0.1 データのダウンロード (前回ダウンロード済みのためスキップ)¶

以下のデータがダウンロード済みです

ファイル名 件数 データセット 備考
rakuten-1000-2023-2024.xlsx.zip 10,000 •レジャー+ビジネスの 10エリア
•エリアごと 1,000件 (ランダムサンプリング)
•期間: 2023/1~2024 GW明け
本講義の全体を通して使用する
In [2]:
# もし、再度ダウンロードが必要な場合は残りの行のコメントマーク「#」を除去して、このセルを再実行してください

# FILE_ID = "1EeCuDrfKdlsMxG9p3Ot7TIxfV9_f2smY"
# !gdown --id {FILE_ID}
# !unzip -o rakuten-1000-2023-2024.xlsx.zip

1.0.2 データの読み込み (DataFrame型)¶

In [3]:
import numpy as np
import pandas as pd

all_df = pd.read_excel("rakuten-1000-2023-2024.xlsx")
print(all_df.shape)
display(all_df.head())
(10000, 18)
カテゴリー エリア 施設番号 施設名 コメント 総合 サービス 立地 部屋 設備・アメニティ 風呂 食事 旅行の目的 同伴者 宿泊年月 投稿者 年代 性別
0 A_レジャー 01_登別 80732 登別カルルス温泉 湯元オロフレ荘 彼のリクエストでカルルス温泉に宿泊させていただきました。北海道ラブ割利用でお得に宿泊できる分... 5 5 4 5 5.0 5.0 5.0 レジャー 恋人 45231 投稿者 na na
1 A_レジャー 01_登別 149334 天然温泉 幸鐘の湯 ドーミーイン東室蘭(ドーミーイン・御宿野乃 ホテルズグループ) 室蘭方面の旅行の時には何度か宿泊しています。ドーミーインのサービスはとても良くて大好きです。... 4 5 5 2 5.0 5.0 5.0 レジャー 家族 45413 投稿者 na na
2 A_レジャー 01_登別 109022 心のリゾート 海の別邸ふる川 もう一度行きたくなる宿でした。部屋もワンランクアップしていただき感謝しています。お風呂も最高... 4 3 5 5 4.0 5.0 3.0 レジャー 一人 45261 gaku0713 60代 男性
3 A_レジャー 01_登別 7506 ホテルニューバジェット室蘭 居心地良く過ごせました 4 4 4 4 4.0 4.0 3.0 レジャー 一人 45200 投稿者 na na
4 A_レジャー 01_登別 37380 登別温泉 旅亭 花ゆら 事前カード精算で露天風呂付特別和洋室を予約しましたが別の和室に案内されました。すぐ違うことに... 1 2 4 2 2.0 4.0 3.0 レジャー 家族 45323 投稿者 na na

1.0.3 「文書-抽出語」表 を作成する¶

コメント列から単語を抽出する (単語を品詞「名詞」「形容詞」「未知語」で絞り込む)

In [4]:
# 必要ライブラリのインポート
from collections import defaultdict
import MeCab

# mecab の初期化
tagger = MeCab.Tagger("-r ../tools/usr/local/etc/mecabrc --unk-feature 未知語")

# 単語頻度辞書の初期化
word_counts = defaultdict(lambda: 0)

# 抽出語情報リストの初期化
words = []

# 半角->全角変換マクロを定義する
ZEN = "".join(chr(0xff01 + i) for i in range(94))
HAN = "".join(chr(0x21 + i) for i in range(94))
HAN2ZEN = str.maketrans(HAN, ZEN)

# ストップワードを定義する
# stopwords = ['する', 'ある', 'ない', 'いう', 'もの', 'こと', 'よう', 'なる', 'ほう']
stopwords = ["湯畑"]

# データ1行ごとのループ
for index, row in all_df.iterrows():

    # 半角->全角変換した後で, mecab で解析する
    node = tagger.parseToNode(row["コメント"].translate(HAN2ZEN))

    # 形態素ごとのループ
    while node:
        # 解析結果を要素ごとにバラす
        features = node.feature.split(',')

        # 品詞1 を取り出す
        pos1 = features[0]

        # 品詞2 を取り出す
        pos2 = features[1] if len(features) > 1 else ""

        # 原形 を取り出す
        base = features[6] if len(features) > 6 else None

        # 原型がストップワードに含まれない単語のみ抽出する
        if base not in stopwords:

            # 「名詞-一般」
            if (pos1 == "名詞" and pos2 == "一般"):
                base = base if base is not None else node.surface
                postag = "名詞"
                key = (base, postag)

                # 単語頻度辞書をカウントアップする
                word_counts[key] += 1

                # 抽出語情報をリストに追加する
                words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])

            # 「形容動詞」
            elif (pos1 == "名詞" and pos2 == "形容動詞語幹"):
                base = base if base is not None else node.surface
                base = f"{base}"
                postag = "形容動詞"
                key = (base, postag)

                # 単語頻度辞書をカウントアップする
                word_counts[key] += 1

                # 抽出語情報をリストに追加する
                words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])

            # 「形容詞」
            elif pos1 == "形容詞":
                base = base if base is not None else node.surface
                postag = "形容詞"
                key = (base, postag)

                # 単語頻度辞書をカウントアップする
                word_counts[key] += 1

                # 抽出語情報をリストに追加する
                words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])

            # 「未知語」
            elif pos1 == "未知語":
                base = base if base is not None else node.surface
                postag = "未知語"
                key = (base, postag)

                # 単語頻度辞書をカウントアップする
                word_counts[key] += 1

                # 抽出語情報をリストに追加する
                words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])

        # 次の形態素へ
        node = node.next

# DataFrme 型に整える
columns = [
    "文書ID",
    # "単語ID",
    "表層",
    "品詞",
    "カテゴリー",
    "エリア",
    "dict_key",
]
docs_df = pd.DataFrame(words, columns=columns)

# DataFrame を表示する
print(docs_df.shape)
display(docs_df.head())
(158822, 6)
文書ID 表層 品詞 カテゴリー エリア dict_key
0 1 リクエスト 名詞 A_レジャー 01_登別 (リクエスト, 名詞)
1 1 温泉 名詞 A_レジャー 01_登別 (温泉, 名詞)
2 1 ラブ 名詞 A_レジャー 01_登別 (ラブ, 名詞)
3 1 割 名詞 A_レジャー 01_登別 (割, 名詞)
4 1 得 名詞 A_レジャー 01_登別 (得, 名詞)

抽出語の出現頻度をカウントする

In [5]:
# 「文書-抽出語」 表から単語の出現回数をカウントする
word_list = []
for i, (k, v) in enumerate(sorted(word_counts.items(), key=lambda x:x[1], reverse=True)):
    word_list.append((i, k[0], v, k))

# DataFrame 型に整える
columns = [
    "単語ID",
    "表層",
    "出現頻度",
    "dict_key"
]

# DataFrame を表示する
word_counts_df = pd.DataFrame(word_list, columns=columns)
print(word_counts_df.shape)
display(word_counts_df.head(10))
(9060, 4)
単語ID 表層 出現頻度 dict_key
0 0 部屋 6768 (部屋, 名詞)
1 1 良い 5397 (良い, 形容詞)
2 2 ホテル 3001 (ホテル, 名詞)
3 3 風呂 2692 (風呂, 名詞)
4 4 ない 2392 (ない, 形容詞)
5 5 美味しい 2294 (美味しい, 形容詞)
6 6 温泉 1854 (温泉, 名詞)
7 7 スタッフ 1677 (スタッフ, 名詞)
8 8 立地 1532 (立地, 名詞)
9 9 よい 1517 (よい, 形容詞)

1.1 カテゴリーやエリアごとのユーザーの注目ポイントを押さえる¶

2.1.1 「文書-抽出語」表の作成¶

「文書-抽出語」表を作成する (出現回数 Top 1000語)

In [6]:
# 「単語出現回数」 表から出現回数Top 1000語のみ抽出する
word_counts_1000_df = word_counts_df[0:1000]

# 「文書-抽出語」 表も出現回数Top 150語のみに絞り込む
merged_df = pd.merge(docs_df, word_counts_1000_df, how="inner", on="dict_key", suffixes=["", "_right"])
docs_1000_df = merged_df[["文書ID", "単語ID", "表層", "品詞", "カテゴリー", "エリア", "dict_key"]]

# 「カテゴリー,エリア」でクロス集計する
cross_1000_df = pd.crosstab(
    [
        docs_1000_df['カテゴリー'], 
        docs_1000_df['エリア'], 
        docs_1000_df['文書ID']
    ],
    docs_1000_df['単語ID'], margins=False
)
cross_1000_df.columns = word_counts_1000_df["表層"]

「文書-抽出語」表を {0,1} に変換する

In [7]:
# 「文書-抽出語」 表を {0,1} に変換する
cross_1000_df[cross_1000_df > 0] = 1

# DataFrame を表示する
print(cross_1000_df.shape)
display(cross_1000_df)
(9914, 1000)
表層 部屋 良い ホテル 風呂 ない 美味しい 温泉 スタッフ 立地 よい ... ほこり 豊か カラオケ 支配人 頻繁 感覚 枕元 コンサート 店舗 人柄
カテゴリー エリア 文書ID
A_レジャー 01_登別 1 1 0 0 0 0 1 1 1 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 1 1 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 1 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
5 1 0 0 1 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
B_ビジネス 10_福岡 9996 1 0 1 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9997 1 0 0 1 0 0 0 0 1 0 ... 0 0 0 0 0 0 0 0 0 0
9998 0 0 1 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9999 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
10000 0 1 1 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

9914 rows × 1000 columns

2.1.2 共起行列を作成する (外部変数-抽出語)¶

In [8]:
# 「カテゴリー」のクロス集計と「エリア」のクロス集計を連結する
aggregate_df = pd.concat(
    [
        cross_1000_df.groupby(level='カテゴリー').sum(),
        cross_1000_df.groupby(level='エリア').sum()
    ]
)

# DataFrame を表示する
print(aggregate_df.shape)
display(aggregate_df)
(12, 1000)
表層 部屋 良い ホテル 風呂 ない 美味しい 温泉 スタッフ 立地 よい ... ほこり 豊か カラオケ 支配人 頻繁 感覚 枕元 コンサート 店舗 人柄
A_レジャー 2367 2134 789 1498 957 1486 1290 916 548 677 ... 4 16 16 13 11 12 5 1 1 17
B_ビジネス 2213 1705 1368 652 789 542 113 505 942 578 ... 11 4 3 3 8 9 15 18 19 3
01_登別 433 422 173 296 193 260 271 149 47 111 ... 1 1 0 0 0 0 0 0 0 2
02_草津 489 468 161 376 198 305 311 177 146 134 ... 1 3 4 3 8 1 0 0 0 5
03_箱根 544 454 158 331 199 373 234 222 69 156 ... 2 5 6 3 0 4 2 0 0 4
04_道後 408 380 228 190 157 177 231 130 184 135 ... 0 0 4 1 0 2 2 0 0 0
05_湯布院 493 410 69 305 210 371 243 238 102 141 ... 0 7 2 6 3 5 1 1 1 6
06_札幌 461 359 278 126 138 135 24 95 191 92 ... 1 1 0 1 3 4 2 4 3 1
07_名古屋 430 337 267 123 155 104 30 99 164 130 ... 1 0 1 0 1 0 4 2 5 0
08_東京 398 345 276 119 162 82 9 100 172 113 ... 3 0 0 1 0 1 5 1 1 1
09_大阪 459 316 262 139 151 100 25 104 184 122 ... 5 1 2 1 3 1 2 4 2 1
10_福岡 465 348 285 145 183 121 25 107 231 121 ... 1 2 0 0 1 3 2 7 8 0

12 rows × 1000 columns

2.1.3 Jaccard 係数を求める (外部変数-抽出語)¶

In [9]:
# 抽出語の出現回数を取得する
word_counts = cross_1000_df.sum(axis=0).values

# 属性(外部変数)出現数を取得する
attr_counts = np.hstack(
    [
        all_df.value_counts('カテゴリー').values,
        all_df.value_counts('エリア').values
    ]
)

# 共起行列の中身を Jaccard 係数に入れ替える
jaccard_attrs_df = gssm_utils.jaccard_attrs_coef(aggregate_df, attr_counts, word_counts, total=10000, conditional=False)

# DataFrame を表示する
print(jaccard_attrs_df.shape)
display(jaccard_attrs_df)
(12, 1000)
表層 部屋 良い ホテル 風呂 ない 美味しい 温泉 スタッフ 立地 よい ... ほこり 豊か カラオケ 支配人 頻繁 感覚 枕元 コンサート 店舗 人柄
A_レジャー 0.328157 0.318270 0.000000 0.265039 0.165314 0.268134 0.252298 0.166394 0.000000 0.121370 ... 0.000000 0.003197 0.003198 0.002598 0.002196 0.002396 0.000000 0.000000 0.000000 0.003398
B_ビジネス 0.000000 0.000000 0.236310 0.000000 0.000000 0.000000 0.000000 0.000000 0.169791 0.000000 ... 0.002198 0.000000 0.000000 0.000000 0.000000 0.000000 0.002997 0.003599 0.003799 0.000000
01_登別 0.000000 0.095540 0.000000 0.103714 0.075597 0.093931 0.127111 0.065581 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
02_草津 0.096052 0.107069 0.000000 0.135544 0.077708 0.112009 0.148662 0.078877 0.000000 0.063178 ... 0.000000 0.002950 0.003941 0.002962 0.007913 0.000000 0.000000 0.000000 0.000000 0.004926
03_箱根 0.108022 0.103535 0.000000 0.117418 0.078131 0.140490 0.107884 0.100955 0.000000 0.074321 ... 0.001974 0.004926 0.005923 0.002962 0.000000 0.003933 0.000000 0.000000 0.000000 0.003937
04_道後 0.000000 0.000000 0.077842 0.000000 0.000000 0.000000 0.106354 0.000000 0.079792 0.063679 ... 0.000000 0.000000 0.003941 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
05_湯布院 0.096914 0.092572 0.000000 0.107206 0.082808 0.139631 0.112500 0.109024 0.000000 0.066698 ... 0.000000 0.006910 0.001967 0.005941 0.002953 0.004921 0.000000 0.000000 0.000000 0.005917
06_札幌 0.090057 0.000000 0.096561 0.000000 0.000000 0.000000 0.000000 0.000000 0.083080 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.002953 0.003933 0.000000 0.003941 0.002950 0.000000
07_名古屋 0.000000 0.000000 0.092388 0.000000 0.000000 0.000000 0.000000 0.000000 0.070507 0.061176 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.003937 0.001967 0.004926 0.000000
08_東京 0.000000 0.000000 0.095800 0.000000 0.000000 0.000000 0.000000 0.000000 0.074202 0.000000 ... 0.002964 0.000000 0.000000 0.000000 0.000000 0.000000 0.004926 0.000000 0.000000 0.000000
09_大阪 0.089631 0.000000 0.090501 0.000000 0.000000 0.000000 0.000000 0.000000 0.079792 0.000000 ... 0.004950 0.000000 0.001967 0.000000 0.002953 0.000000 0.000000 0.003941 0.000000 0.000000
10_福岡 0.090909 0.000000 0.099234 0.000000 0.071401 0.000000 0.000000 0.000000 0.102258 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.002947 0.000000 0.006917 0.007905 0.000000

12 rows × 1000 columns

2.1.4 特徴語ランキング¶

In [10]:
# 「カテゴリ」や「エリア」ごとに Jaccard 係数のTop 10語を抽出する
df_list = []
for index, row in jaccard_attrs_df.iterrows():
    df_list.append(row.iloc[np.argsort(row.values)[::-1][:10]].reset_index())

# 「カテゴリ」や「エリア」ごとの Jaccard 係数のTop 10 を横方向に連結した DetaFrame を作成する
ranking_df = pd.DataFrame(pd.concat(df_list, axis=1))
ranking_df.columns = np.array([c for pair in [[x,f"jaccard"] for x in jaccard_attrs_df.index] for c in pair], dtype='object')

# DataFrame を表示する
display(ranking_df)
A_レジャー jaccard B_ビジネス jaccard 01_登別 jaccard 02_草津 jaccard 03_箱根 jaccard ... 06_札幌 jaccard 07_名古屋 jaccard 08_東京 jaccard 09_大阪 jaccard 10_福岡 jaccard
0 部屋 0.328157 ホテル 0.236310 温泉 0.127111 温泉 0.148662 美味しい 0.140490 ... ホテル 0.096561 ホテル 0.092388 駅 0.121037 駅 0.092697 立地 0.102258
1 良い 0.318270 立地 0.169791 風呂 0.103714 風呂 0.135544 露天風呂 0.132890 ... 部屋 0.090057 駅 0.084774 便利 0.097297 ホテル 0.090501 ホテル 0.099234
2 美味しい 0.268134 駅 0.153921 良い 0.095540 宿 0.117160 風呂 0.117418 ... 立地 0.083080 便利 0.072372 ホテル 0.095800 部屋 0.089631 部屋 0.090909
3 風呂 0.265039 便利 0.145952 美味しい 0.093931 美味しい 0.112009 部屋 0.108022 ... 浴場 0.074408 立地 0.070507 コンビニ 0.082481 便利 0.083823 駅 0.085379
4 温泉 0.252298 フロント 0.115707 バイキング 0.084771 良い 0.107069 温泉 0.107884 ... 便利 0.071806 近い 0.070175 近い 0.080283 立地 0.079792 便利 0.084402
5 スタッフ 0.166394 近い 0.112383 夕食 0.084353 部屋 0.096052 宿 0.107331 ... 快適 0.065593 快適 0.069420 立地 0.074202 フロント 0.073434 ない 0.071401
6 ない 0.165314 綺麗 0.104125 種類 0.078832 湯 0.094293 良い 0.103535 ... 広い 0.063860 フロント 0.066524 フロント 0.067669 近い 0.073314 綺麗 0.067352
7 宿 0.159968 快適 0.095608 ない 0.075597 最高 0.084139 スタッフ 0.100955 ... 駅 0.061681 よい 0.061176 アメニティ 0.063199 綺麗 0.063525 フロント 0.064810
8 露天風呂 0.132394 コンビニ 0.087794 最高 0.071951 夕食 0.081567 夕食 0.097068 ... フロント 0.060832 コンビニ 0.060788 綺麗 0.057026 快適 0.060534 近い 0.063953
9 夕食 0.124876 アメニティ 0.076083 残念 0.071800 スタッフ 0.078877 残念 0.078534 ... 近い 0.056582 綺麗 0.059184 快適 0.056147 コンビニ 0.057961 バス 0.058943

10 rows × 24 columns

2.1.5 ワードクラウド¶

In [11]:
import matplotlib.pyplot as plt
%matplotlib inline

# サブルーチン
def sort_and_plot(name, group):

    # 「カテゴリー」ごとに Jaccard 係数でソートする
    sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]

    # Jaccard 係数Top 75語をソートして抽出する
    group_cross_df = group.iloc[:,sorted_columns]

    # プロットする
    ax = fig.add_subplot(4, 3, i+1)
    gssm_utils.plot_wordcloud_ax(ax, " ".join(group_cross_df.columns))
    ax.set_title(name)


# プロットの準備
fig = plt.figure(figsize=(12, 9))

i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
    # サブルーチンを呼ぶ
    sort_and_plot(name, group)
    i += 1

    # エリアごとのループ
    for sub_name, sub_group in group.groupby(level='エリア'):
        # サブルーチンを呼ぶ
        sort_and_plot(sub_name, sub_group)
        i += 1

# プロットの仕上げ
plt.tight_layout()
plt.show()
No description has been provided for this image

2.1.6 共起ネットワーク図 (カテゴリ)¶

In [12]:
# 抽出語の出現回数を取得する
word_counts = cross_1000_df.sum(axis=0).values

# 属性(外部変数)出現数を取得する
attr_counts = np.hstack(
    [
        all_df.value_counts('カテゴリー').values,
    ]
)

# カテゴリのみの共起行列(共起度数)を取得する
df = aggregate_df.loc[["A_レジャー","B_ビジネス"],:]

# 共起行列(共起度数)で共起ネットワーク図を作成する
gssm_utils.plot_attrs_network(df, attr_counts, word_counts, np.sort(df.values.reshape(-1))[::-1][60], width=8, height=8)
No description has been provided for this image

2.1.7 共起ネットワーク図 (エリア)¶

In [13]:
# 抽出語の出現回数を取得する
word_counts = cross_1000_df.sum(axis=0).values

# 属性(外部変数)出現数を取得する
attr_counts = np.hstack(
    [
        all_df.value_counts('エリア').values,
    ]
)

# カテゴリのみの共起行列(共起度数)を取得する
df = aggregate_df.iloc[2:,:]

# 共起行列((共起度数)で共起ネットワーク図を作成する
gssm_utils.plot_attrs_network(df, attr_counts, word_counts, np.sort(df.values.reshape(-1))[::-1][120], width=8, height=8)
No description has been provided for this image

1.2 カテゴリーやエリアごとのユーザーの注目ポイントの評価の違いを見つける¶

1.1.1 「文書-抽出語」表の作成¶

「文書-抽出語」表を作成する (出現回数 Top 1000語)

In [14]:
# 「単語出現回数」 表から出現回数Top 1000語のみ抽出する
word_counts_1000_df = word_counts_df[0:1000]

# 「文書-抽出語」 表も出現回数Top 150語のみに絞り込む
merged_df = pd.merge(docs_df, word_counts_1000_df, how="inner", on="dict_key", suffixes=["", "_right"])
docs_1000_df = merged_df[["文書ID", "単語ID", "表層", "品詞", "カテゴリー", "エリア", "dict_key"]]

# 「カテゴリー,エリア」でクロス集計する
cross_1000_df = pd.crosstab(
    [
        docs_1000_df['カテゴリー'], 
        docs_1000_df['エリア'], 
        docs_1000_df['文書ID']
    ],
    docs_1000_df['単語ID'], margins=False
)
cross_1000_df.columns = word_counts_1000_df["表層"]

「文書-抽出語」表を {0,1} に変換する

In [15]:
# 「文書-抽出語」 表を {0,1} に変換する
cross_1000_df[cross_1000_df > 0] = 1

# DataFrame を表示する
print(cross_1000_df.shape)
display(cross_1000_df)
(9914, 1000)
表層 部屋 良い ホテル 風呂 ない 美味しい 温泉 スタッフ 立地 よい ... ほこり 豊か カラオケ 支配人 頻繁 感覚 枕元 コンサート 店舗 人柄
カテゴリー エリア 文書ID
A_レジャー 01_登別 1 1 0 0 0 0 1 1 1 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 1 1 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 1 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 1 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
5 1 0 0 1 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
B_ビジネス 10_福岡 9996 1 0 1 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9997 1 0 0 1 0 0 0 0 1 0 ... 0 0 0 0 0 0 0 0 0 0
9998 0 0 1 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9999 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
10000 0 1 1 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

9914 rows × 1000 columns

1.1.2 共起行列を作成する (外部変数-抽出語)¶

In [16]:
# 「カテゴリー」のクロス集計と「エリア」のクロス集計を連結する
aggregate_df = pd.concat(
    [
        cross_1000_df.groupby(level='カテゴリー').sum(),
        cross_1000_df.groupby(level='エリア').sum()
    ]
)

# DataFrame を表示する
print(aggregate_df.shape)
display(aggregate_df)
(12, 1000)
表層 部屋 良い ホテル 風呂 ない 美味しい 温泉 スタッフ 立地 よい ... ほこり 豊か カラオケ 支配人 頻繁 感覚 枕元 コンサート 店舗 人柄
A_レジャー 2367 2134 789 1498 957 1486 1290 916 548 677 ... 4 16 16 13 11 12 5 1 1 17
B_ビジネス 2213 1705 1368 652 789 542 113 505 942 578 ... 11 4 3 3 8 9 15 18 19 3
01_登別 433 422 173 296 193 260 271 149 47 111 ... 1 1 0 0 0 0 0 0 0 2
02_草津 489 468 161 376 198 305 311 177 146 134 ... 1 3 4 3 8 1 0 0 0 5
03_箱根 544 454 158 331 199 373 234 222 69 156 ... 2 5 6 3 0 4 2 0 0 4
04_道後 408 380 228 190 157 177 231 130 184 135 ... 0 0 4 1 0 2 2 0 0 0
05_湯布院 493 410 69 305 210 371 243 238 102 141 ... 0 7 2 6 3 5 1 1 1 6
06_札幌 461 359 278 126 138 135 24 95 191 92 ... 1 1 0 1 3 4 2 4 3 1
07_名古屋 430 337 267 123 155 104 30 99 164 130 ... 1 0 1 0 1 0 4 2 5 0
08_東京 398 345 276 119 162 82 9 100 172 113 ... 3 0 0 1 0 1 5 1 1 1
09_大阪 459 316 262 139 151 100 25 104 184 122 ... 5 1 2 1 3 1 2 4 2 1
10_福岡 465 348 285 145 183 121 25 107 231 121 ... 1 2 0 0 1 3 2 7 8 0

12 rows × 1000 columns

1.1.3 Jaccard 係数を求める (外部変数-抽出語)¶

In [17]:
# 抽出語の出現回数を取得する
word_counts = cross_1000_df.sum(axis=0).values

# 属性(外部変数)出現数を取得する
attr_counts = np.hstack(
    [
        all_df.value_counts('カテゴリー').values,
        all_df.value_counts('エリア').values
    ]
)

# 共起行列の中身を Jaccard 係数に入れ替える
jaccard_attrs_df = gssm_utils.jaccard_attrs_coef(aggregate_df, attr_counts, word_counts, total=10000, conditional=False)

# DataFrame を表示する
print(jaccard_attrs_df.shape)
display(jaccard_attrs_df)
(12, 1000)
表層 部屋 良い ホテル 風呂 ない 美味しい 温泉 スタッフ 立地 よい ... ほこり 豊か カラオケ 支配人 頻繁 感覚 枕元 コンサート 店舗 人柄
A_レジャー 0.328157 0.318270 0.000000 0.265039 0.165314 0.268134 0.252298 0.166394 0.000000 0.121370 ... 0.000000 0.003197 0.003198 0.002598 0.002196 0.002396 0.000000 0.000000 0.000000 0.003398
B_ビジネス 0.000000 0.000000 0.236310 0.000000 0.000000 0.000000 0.000000 0.000000 0.169791 0.000000 ... 0.002198 0.000000 0.000000 0.000000 0.000000 0.000000 0.002997 0.003599 0.003799 0.000000
01_登別 0.000000 0.095540 0.000000 0.103714 0.075597 0.093931 0.127111 0.065581 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
02_草津 0.096052 0.107069 0.000000 0.135544 0.077708 0.112009 0.148662 0.078877 0.000000 0.063178 ... 0.000000 0.002950 0.003941 0.002962 0.007913 0.000000 0.000000 0.000000 0.000000 0.004926
03_箱根 0.108022 0.103535 0.000000 0.117418 0.078131 0.140490 0.107884 0.100955 0.000000 0.074321 ... 0.001974 0.004926 0.005923 0.002962 0.000000 0.003933 0.000000 0.000000 0.000000 0.003937
04_道後 0.000000 0.000000 0.077842 0.000000 0.000000 0.000000 0.106354 0.000000 0.079792 0.063679 ... 0.000000 0.000000 0.003941 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
05_湯布院 0.096914 0.092572 0.000000 0.107206 0.082808 0.139631 0.112500 0.109024 0.000000 0.066698 ... 0.000000 0.006910 0.001967 0.005941 0.002953 0.004921 0.000000 0.000000 0.000000 0.005917
06_札幌 0.090057 0.000000 0.096561 0.000000 0.000000 0.000000 0.000000 0.000000 0.083080 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.002953 0.003933 0.000000 0.003941 0.002950 0.000000
07_名古屋 0.000000 0.000000 0.092388 0.000000 0.000000 0.000000 0.000000 0.000000 0.070507 0.061176 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.003937 0.001967 0.004926 0.000000
08_東京 0.000000 0.000000 0.095800 0.000000 0.000000 0.000000 0.000000 0.000000 0.074202 0.000000 ... 0.002964 0.000000 0.000000 0.000000 0.000000 0.000000 0.004926 0.000000 0.000000 0.000000
09_大阪 0.089631 0.000000 0.090501 0.000000 0.000000 0.000000 0.000000 0.000000 0.079792 0.000000 ... 0.004950 0.000000 0.001967 0.000000 0.002953 0.000000 0.000000 0.003941 0.000000 0.000000
10_福岡 0.090909 0.000000 0.099234 0.000000 0.071401 0.000000 0.000000 0.000000 0.102258 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.002947 0.000000 0.006917 0.007905 0.000000

12 rows × 1000 columns

2.1.6 共起ネットワーク図¶

In [18]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline

# サブルーチン
def sort_and_plot(name, group):

    # 「カテゴリー」ごとに Jaccard 係数でソートする
    sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]

    # Jaccard 係数Top 75語をソートして抽出する
    group_cross_df = group.iloc[:,sorted_columns]

    # 共起行列を作成する
    X = group_cross_df.values
    X = csc_matrix(X)
    Xc = (X.T * X)
    Xc = np.triu(Xc.toarray())

    # 共起行列を DataFrame に整える
    group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)

    # 共起行列の中身を Jaccard 係数に入れ替える
    group_jaccard_df = gssm_utils.jaccard_coef(group_cooccur_df, group_cross_df)

    # 抽出語の出現回数を取得する
    word_counts = group_cross_df.sum(axis=0).values

    # プロットする
    ax = fig.add_subplot(4, 3, i+1)
    gssm_utils.plot_cooccur_network_ax(ax, group_jaccard_df, word_counts, np.sort(group_jaccard_df.values.reshape(-1))[::-1][120], html=True)
    ax.set_title(name)


# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))

i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
    # サブルーチンを呼ぶ
    sort_and_plot(name, group)
    i += 1

    # エリアごとのループ
    for sub_name, sub_group in group.groupby(level='エリア'):
        # サブルーチンを呼ぶ
        sort_and_plot(sub_name, sub_group)
        i += 1

# プロットの仕上げ
plt.tight_layout()
plt.show()
plot_cooccur_network-20240707133409.450.html
plot_cooccur_network-20240707133409.914.html
plot_cooccur_network-20240707133410.342.html
plot_cooccur_network-20240707133410.733.html
plot_cooccur_network-20240707133411.637.html
plot_cooccur_network-20240707133412.068.html
plot_cooccur_network-20240707133412.635.html
plot_cooccur_network-20240707133413.647.html
plot_cooccur_network-20240707133414.533.html
plot_cooccur_network-20240707133415.378.html
plot_cooccur_network-20240707133416.132.html
plot_cooccur_network-20240707133416.798.html
No description has been provided for this image

1.3 高評価のエリアに倣って、低評価のエリアを改善するプランを提案する¶

1.2.0 対照的な2エリアを選択する¶

In [19]:
# コーディングルール
coding_pos = ["良い","美味しい","広い","多い","素晴らしい","嬉しい","気持ちよい","楽しい","近い","大きい","気持ち良い","温かい","早い","優しい","新しい","暖かい","快い","明るい","美しい","可愛い","満足"]
coding_neg = ["古い","無い","高い","悪い","小さい","狭い","少ない","寒い","遅い","熱い","欲しい","暑い","冷たい","遠い","臭い","暗い","うるさい","ない","無い","残念","改善","不満"]
In [20]:
# DataFrame を初期化する
cross_1000_ps_df = cross_1000_df.copy()
cross_1000_ps_df['ポジ'] = 0
cross_1000_ps_df['ネガ'] = 0
cross_1000_ps_df['総合1-2'] = 0
cross_1000_ps_df['総合4-5'] = 0

# コーディングルールを適用する (ポジ・ネガ)
pos_index = docs_df['表層'].str.contains("|".join(coding_pos))
neg_index = docs_df['表層'].str.contains("|".join(coding_neg))
cross_1000_ps_df.loc[cross_1000_ps_df.index.get_level_values('文書ID').isin(docs_df.loc[pos_index, '文書ID']), 'ポジ'] = 1
cross_1000_ps_df.loc[cross_1000_ps_df.index.get_level_values('文書ID').isin(docs_df.loc[neg_index, '文書ID']), 'ネガ'] = 1

# コーディングルールを適用する (総合評価)
cross_1000_ps_df.loc[cross_1000_ps_df.index.get_level_values('文書ID').isin(all_df[all_df['総合'] <=2].index), '総合1-2'] = 1
cross_1000_ps_df.loc[cross_1000_ps_df.index.get_level_values('文書ID').isin(all_df[all_df['総合'] >=4].index), '総合4-5'] = 1
cross_1000_ps_df = cross_1000_ps_df[['ポジ','ネガ','総合1-2','総合4-5']]

# DataFrame を表示する
print(cross_1000_ps_df.shape)
display(cross_1000_ps_df)
(9914, 4)
表層 ポジ ネガ 総合1-2 総合4-5
カテゴリー エリア 文書ID
A_レジャー 01_登別 1 1 0 0 1
2 1 1 0 1
3 0 0 0 1
4 1 0 1 0
5 1 1 0 1
... ... ... ... ... ... ...
B_ビジネス 10_福岡 9996 1 0 0 1
9997 0 1 0 1
9998 0 0 0 1
9999 1 0 0 1
10000 1 0 0 0

9914 rows × 4 columns

In [21]:
# 「カテゴリー」のクロス集計と「エリア」のクロス集計を連結する
aggregate_ps_df = pd.concat(
    [
        cross_1000_ps_df.groupby(level='カテゴリー').sum(),
        cross_1000_ps_df.groupby(level='エリア').sum()
    ]
)

# DataFrame を表示する
print(aggregate_ps_df.shape)
display(aggregate_ps_df)
(12, 4)
表層 ポジ ネガ 総合1-2 総合4-5
A_レジャー 3798 2297 282 4279
B_ビジネス 3230 2105 289 4032
01_登別 729 472 69 822
02_草津 803 497 56 843
03_箱根 793 495 63 857
04_道後 668 405 47 854
05_湯布院 805 428 47 903
06_札幌 666 399 48 825
07_名古屋 642 435 40 828
08_東京 641 421 70 776
09_大阪 655 409 52 815
10_福岡 626 441 79 788
In [22]:
# 必要ライブラリのインポート
import mca

# ライブラリ mca による対応分析
ncols = aggregate_ps_df.shape[1]
mca_ben = mca.MCA(aggregate_ps_df, ncols=ncols, benzecri=False)

# 行方向および列方向の値を取り出す
row_coord = mca_ben.fs_r(N=2)
col_coord = mca_ben.fs_c(N=2)

# 固有値を求める
eigenvalues = mca_ben.L
total = np.sum(eigenvalues)
# 寄与率を求める
explained_inertia = 100 * eigenvalues / total

# 行方向および列方向のラベルを取得する
row_labels = aggregate_ps_df.index
col_labels = aggregate_ps_df.columns

# プロットする
gssm_utils.plot_coresp(row_coord, col_coord, row_labels, col_labels, explained_inertia)
No description has been provided for this image

1.2.1 ポジティブ意見の「文書-抽出語」表を作成する¶

In [23]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline

# コーディングルール
coding_or = coding_pos

# サブルーチン
def sort_and_plot(name, group):

    # 「カテゴリー」ごとに Jaccard 係数でソートする
    sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]

    # Jaccard 係数Top 75語をソートして抽出する
    group_cross_df = group.iloc[:,sorted_columns]

    # 共起行列を作成する
    X = group_cross_df.values
    X = csc_matrix(X)
    Xc = (X.T * X)
    Xc = np.triu(Xc.toarray())

    # コーディングルールで絞り込む
    index = docs_df['表層'].str.contains("|".join(coding_or))
    group_cross_df = group_cross_df[group_cross_df.index.get_level_values('文書ID').isin(docs_df.loc[index, '文書ID'])]

    # 共起行列を DataFrame に整える
    group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)

    # 共起行列の中身を Jaccard 係数に入れ替える
    group_jaccard_df = gssm_utils.jaccard_coef(group_cooccur_df, group_cross_df)

    # 抽出語の出現回数を取得する
    word_counts = group_cross_df.sum(axis=0).values

    # プロットする
    ax = fig.add_subplot(4, 3, i+1)
    gssm_utils.plot_cooccur_network_with_code_ax(ax, group_jaccard_df, word_counts, np.sort(group_jaccard_df.values.reshape(-1))[::-1][120], coding_or, html=True)
    ax.set_title(name)


# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))

i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
    # サブルーチンを呼ぶ
    sort_and_plot(name, group)
    i += 1

    # エリアごとのループ
    for sub_name, sub_group in group.groupby(level='エリア'):
        # サブルーチンを呼ぶ
        sort_and_plot(sub_name, sub_group)
        i += 1

# プロットの仕上げ
plt.tight_layout()
plt.show()
plot_cooccur_network_with_code-20240707133421.300.html
plot_cooccur_network_with_code-20240707133421.893.html
plot_cooccur_network_with_code-20240707133422.321.html
plot_cooccur_network_with_code-20240707133422.871.html
plot_cooccur_network_with_code-20240707133423.776.html
plot_cooccur_network_with_code-20240707133424.203.html
plot_cooccur_network_with_code-20240707133424.858.html
plot_cooccur_network_with_code-20240707133425.783.html
plot_cooccur_network_with_code-20240707133426.727.html
plot_cooccur_network_with_code-20240707133427.695.html
plot_cooccur_network_with_code-20240707133428.500.html
plot_cooccur_network_with_code-20240707133429.089.html
No description has been provided for this image

1.2.2 ネガティブ意見の「文書-抽出語」表を作成する¶

In [ ]:
# 必要ライブラリのインポート
from scipy.sparse import csc_matrix
import matplotlib.pyplot as plt
%matplotlib inline

# コーディングルール
coding_or = coding_neg

# サブルーチン
def sort_and_plot(name, group):

    # 「カテゴリー」ごとに Jaccard 係数でソートする
    sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]

    # Jaccard 係数Top 75語をソートして抽出する
    group_cross_df = group.iloc[:,sorted_columns]

    # 共起行列を作成する
    X = group_cross_df.values
    X = csc_matrix(X)
    Xc = (X.T * X)
    Xc = np.triu(Xc.toarray())

    # コーディングルールで絞り込む
    index = docs_df['表層'].str.contains("|".join(coding_or))
    group_cross_df = group_cross_df[group_cross_df.index.get_level_values('文書ID').isin(docs_df.loc[index, '文書ID'])]

    # 共起行列を DataFrame に整える
    group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)

    # 共起行列の中身を Jaccard 係数に入れ替える
    group_jaccard_df = gssm_utils.jaccard_coef(group_cooccur_df, group_cross_df)

    # 抽出語の出現回数を取得する
    word_counts = group_cross_df.sum(axis=0).values

    # プロットする
    ax = fig.add_subplot(4, 3, i+1)
    gssm_utils.plot_cooccur_network_with_code_ax(ax, group_jaccard_df, word_counts, np.sort(group_jaccard_df.values.reshape(-1))[::-1][120], coding_or, html=True)
    ax.set_title(name)


# プロットの準備
# fig = plt.figure(figsize=(20, 28))
fig = plt.figure(figsize=(28, 28))

i = 0
# カテゴリごとのループ
for name, group in cross_1000_df.groupby(level='カテゴリー'):
    # サブルーチンを呼ぶ
    sort_and_plot(name, group)
    i += 1

    # エリアごとのループ
    for sub_name, sub_group in group.groupby(level='エリア'):
        # サブルーチンを呼ぶ
        sort_and_plot(sub_name, sub_group)
        i += 1

# プロットの仕上げ
plt.tight_layout()
plt.show()
plot_cooccur_network_with_code-20240707133432.739.html
plot_cooccur_network_with_code-20240707133433.233.html
plot_cooccur_network_with_code-20240707133433.699.html
plot_cooccur_network_with_code-20240707133434.033.html
plot_cooccur_network_with_code-20240707133434.792.html
plot_cooccur_network_with_code-20240707133435.095.html
plot_cooccur_network_with_code-20240707133435.485.html
plot_cooccur_network_with_code-20240707133436.501.html
plot_cooccur_network_with_code-20240707133437.238.html
plot_cooccur_network_with_code-20240707133437.810.html
plot_cooccur_network_with_code-20240707133438.432.html
plot_cooccur_network_with_code-20240707133438.881.html

1.2.3 本文の参照 (カテゴリーごと)¶

「登別」と「道後」で「すばらしい」という単語が含まれている口コミを表示する

In [ ]:
# 検索条件
search_index = \
    all_df['エリア'].isin(['01_登別', '05_道後']) & \
    (all_df['コメント'].str.contains('素晴らしい') | all_df['コメント'].str.contains('すばらしい'))

# 検索する
result_df = all_df[search_index]

# DataFrame を表示する
print(result_df.shape)
display(result_df.head())

# CSV に保存する
result_df.to_csv("output-1.csv", header=True)

「東京」と「福岡」で「うるさい」という単語が含まれている口コミを表示する

In [ ]:
# 検索条件
search_index = \
    all_df['エリア'].isin(['08_東京', '10_福岡']) & \
    all_df['コメント'].str.contains('うるさい')

# 検索する
result_df = all_df[search_index]

# DataFrame を表示する
print(result_df.shape)
display(result_df.head())

# CSV に保存する
result_df.to_csv("output-2.csv", header=True)